package com.nulldev.util.web.HttpServer3.extensions.websocket;

import java.nio.ByteBuffer;
import java.security.MessageDigest;
import java.util.List;
import java.util.Properties;

import com.nulldev.util.data.Arrays.arrays.FastLists;
import com.nulldev.util.data.Base64.Base64;
import com.nulldev.util.web.HttpServer3.extensions.websocket.exceptions.UnsupportedCodeException;

public class ProtocolUtils {
	public static final String WS_MAGIC_STRING = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";

	public static final String getMagicPayload(final String clientKey) {
		try {
			return Base64.encodeBase64String(MessageDigest.getInstance("SHA1").digest((clientKey + WS_MAGIC_STRING).getBytes()));
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	enum Code {

		NONE(0),
		TEXT_MESSAGE(1),
		BINARY_MESSAGE(2),

		CLOSE(8),
		PING(9),
		PONG(10);

		private int code;

		Code(int code) {
			this.code = code;
		}

		public int getCode() {
			return code;
		}

		public static Code get(final byte b) {
			for (final Code c : values()) {
				if (c.code == b)
					return c;
			}
			return NONE;
		}
	}

	public static Properties readHandshake(ByteBuffer buffer) {
		Properties props = new Properties();
		StringBuilder sb = new StringBuilder();
		while (buffer.hasRemaining()) {
			sb.append((char) (buffer.get() & 0xff));
		}

		String[] lines = sb.toString().split("\\n");
		for (String line : lines) {
			String[] keyVal = line.split(":");
			if (keyVal.length == 2) {
				props.put(keyVal[0].trim(), keyVal[1].trim());
			}
		}
		return props;
	}

	public static Code getFrameCode(final ByteBuffer buffer) {
		return Code.get((byte) (buffer.get() & 127 & 0xff));
	}

	public static String decodeMaskedFrame(final ByteBuffer buffer) throws UnsupportedCodeException {
		final StringBuilder sb = new StringBuilder();
		final List<Byte> frame = FastLists.list();
		while (buffer.hasRemaining()) {
			frame.add(buffer.get());
		}

		final Byte code = (byte) (frame.remove(0) & 127 & 0xff);

		if (code == Code.TEXT_MESSAGE.getCode()) {
			int length = (int) frame.remove(0) & 127 & 0xff; // 7 bits
			if (length == 126) { // 16 bits
				length = (int) frame.remove(0) & 0xff;
				length += (int) frame.remove(0) & 0xff;
			}
			if (length == 127) { // 64 bits
				length = (int) frame.remove(0) & 0xff;
				length += (int) frame.remove(0) & 0xff;
				length += (int) frame.remove(0) & 0xff;
				length += (int) frame.remove(0) & 0xff;
				length += (int) frame.remove(0) & 0xff;
				length += (int) frame.remove(0) & 0xff;
				length += (int) frame.remove(0) & 0xff;
				length += (int) frame.remove(0) & 0xff;
			}

			List<Byte> masks = frame.subList(0, 4);
			List<Byte> data = frame.subList(4, frame.size());
			for (int i = 0; i < length; i++) {
				sb.append((char) (data.get(i) ^ masks.get(i % masks.size())));
			}
		} else if (code == Code.CLOSE.getCode()) {
			sb.append(Code.CLOSE.name());
		} else {
			throw new UnsupportedCodeException("Websocket frame code: '" + code + "' is not supported.");
		}

		return sb.toString();
	}

	public static ByteBuffer encodeUnmaskedFrame(final Code code, final String message) {
		if (code == null || message == null)
			throw new NullPointerException();
		final List<Byte> frame = FastLists.list();
		frame.add((byte) ((short) code.getCode() | (1 << 7) & 0xff));
		frame.add((byte) message.length());
		for (int i = 0; i < message.length(); i++) {
			frame.add((byte) message.charAt(i));
		}

		final ByteBuffer buffer = ByteBuffer.allocate(frame.size());
		for (byte b : frame) {
			buffer.put(b);
		}

		return buffer;
	}
}
